Runtime Flow
This guide documents Agentty's runtime data flows end to end: the foreground event loop, reducer/event buses, session-worker turn execution, merge/rebase/sync orchestration, and every background task with trigger points and side effects.
Architecture Goals🔗
Agentty runtime design is built around these constraints:
- Keep domain logic independent from infrastructure and UI.
- Keep long-running or external operations behind trait boundaries for testability.
- Keep runtime event handling responsive by offloading background work to async tasks.
- Keep AI-session changes isolated in git worktrees and reviewable as diffs.
- Decouple agent transport (CLI subprocess vs app-server RPC) behind a unified channel abstraction.
Workspace Map🔗
| Path | Responsibility |
|---|---|
crates/agentty/ | Main TUI application crate (agentty) with runtime, app orchestration, domain, infrastructure, and UI modules. |
crates/ag-xtask/ | Workspace maintenance commands (index checks, migration checks, automation helpers). |
docs/site/content/docs/ | End-user and contributor documentation published at /docs/. |
Main Runtime Flow🔗
Primary foreground path from process start to one event-loop cycle:
main.rs
├─ Database::open(...) // sqlite open + WAL + FK + migrations
├─ RoutingAppServerClient::new() // Codex/Gemini router
├─ App::new(...)
│ ├─ load startup project/session snapshots
│ ├─ fail unfinished operations from previous run
│ └─ spawn app background tasks
└─ runtime::run(&mut app)
├─ terminal::setup_terminal()
├─ event::spawn_event_reader(...) // dedicated OS thread
└─ run_main_loop(...)
├─ sessions.sync_from_handles() // pull Arc<Mutex> runtime state into snapshots
├─ ui::render::draw(...)
└─ event::process_events(...)
├─ key events -> mode handlers -> app/session orchestration
├─ app events -> App::apply_app_events reducer
└─ tick -> refresh_sessions_if_needed safety pollrun_main_loop()renders every cycle and applies snapshot sync before draw.process_events()waits on terminal events, app events, or tick (tokio::select!).- After one event, it drains queued terminal events immediately to avoid one-key-per-frame lag.
- Tick interval is
50ms; metadata-based session reload fallback is5s(SESSION_REFRESH_INTERVAL).
Data Channels🔗
Agentty uses four primary runtime data channels:
| Channel | Producer(s) | Consumer(s) | Payload | Purpose |
|---|---|---|---|---|
Terminal Event channel (runtime/event.rs) | Event-reader thread | runtime::process_events() | crossterm::Event | User input and terminal events. |
App event bus (AppEvent) | App background tasks, workers, task helpers | App::apply_app_events() reducer | AppEvent variants | Safe cross-task app-state mutation. |
Turn event stream (TurnEvent) | AgentChannel implementations | Session worker consume_turn_events() | Stream deltas/progress/pid | Real-time turn output and progress updates. |
Session handles (SessionHandles) | Workers/session task helpers | SessionState::sync_from_handles() | Shared Arc<Mutex<...>> output/status/pid | Fast snapshot sync without full DB reload. |
App Event Reducer Flow🔗
App::apply_app_events() is the single reducer path for async app events.
Flow:
- Drain queued events (
first_event+try_recvloop). - Reduce into
AppEventBatch(coalesces refresh, git status, model/progress updates). - Apply side effects in stable order.
Reducer behaviors that matter for data flow:
RefreshSessionssetsshould_force_reload, which triggersrefresh_sessions_now()andreload_projects().SessionUpdatedmarks touched sessions so reducer can callsync_session_from_handle()selectively.SessionProgressUpdatedupdates transient progress labels used by UI.AgentResponseReceivedroutes question-mode transitions for active view sessions.- After touched-session sync, terminal statuses (
Done,Canceled) drop per-session worker senders so workers can shut down runtimes.
Session Turn Data Flow🔗
From prompt submit to persisted result:
- Prompt mode submits:
start_session()for first prompt (TurnMode::Start) orreply()for follow-up (TurnMode::Resume).- Session command is persisted in
session_operationbefore enqueue. SessionWorkerServicelazily creates or reuses a per-session worker queue.- Worker marks operation
running, checks cancel flags, then runs channel turn. - Worker creates
TurnRequest(reasoning level, model, prompt, replay output, provider conversation id). - Worker spawns
consume_turn_events()and sets initial progress (Thinking). AgentChannel::run_turn()streamsTurnEventvalues and returnsTurnResult.- Worker applies final result:
- Append final assistant transcript output when no assistant chunks were already streamed (
answertext, fallbackquestiontext). - Persist session questions and emit
AppEvent::AgentResponseReceived. - Persist stats and per-model usage.
- Persist provider conversation id (app-server providers).
- Run auto-commit assistance path.
- Refresh persisted session size.
- Update final status (
RevieworQuestion; on failure ->Review).
Operation Lifecycle and Recovery🔗
Turn execution is durable and restart-safe:
- Before enqueue: insert
session_operationrow (queued). - Worker transitions:
queued -> running -> done/failed/canceled. - Cancel requests are persisted and checked before command execution.
- On startup, unfinished operations are failed with reason
Interrupted by app restart, and impacted sessions are reset toReview.
Status Transition Rules🔗
Runtime status transitions enforced by Status::can_transition_to():
New -> InProgress(first prompt)Review/Question -> InProgress(reply)Review -> Queued -> Merging -> Done(merge queue path)Review -> Rebasing -> Review/Question(rebase path)Review/Question -> CanceledInProgress/Rebasing -> Review/Question(post-turn or post-rebase)
Agent Channel Architecture🔗
Session workers are transport-agnostic through AgentChannel:
app/session/workflow/worker.rs
└─ AgentChannel::run_turn(session_id, TurnRequest, event_tx)
├─ CliAgentChannel (Claude; subprocess per turn)
└─ AppServerAgentChannel (Codex/Gemini; persistent runtime per session)
└─ AppServerClient
└─ RoutingAppServerClient
├─ RealCodexAppServerClient
└─ RealGeminiAcpClient| Type | Purpose |
|---|---|
TurnRequest | Input payload: reasoning_level, folder, live_session_output, model, mode (start/resume), prompt, provider_conversation_id. |
TurnEvent | Incremental stream events: AssistantDelta, ThoughtDelta, Progress, Completed, Failed, PidUpdate. |
TurnResult | Normalized output: assistant_message, token counts, provider_conversation_id. |
TurnMode | Start (fresh turn) or Resume (with optional session output replay). |
Provider conversation id flow:
- App-server providers return
provider_conversation_idinTurnResult. - Worker persists it to DB (
update_session_provider_conversation_id). - Future
TurnRequestloads and forwards it so runtime restarts can resume native provider context.
Agent Interaction Protocol Flow🔗
Provider output is normalized to one structured response protocol:
- Prompt builders prepend protocol instructions (
answer/questionschema). - Channels stream deltas/progress as
TurnEvent. - Final output is parsed to protocol
messages. - Worker persists final display text and question payloads, then emits
AgentResponseReceived.
Streaming behavior differs by transport/provider:
- CLI channel (
CliAgentChannel): parses stdout lines intoAssistantDeltaandProgress; keeps raw output for final parse. - App-server channel (
AppServerAgentChannel): bridgesAppServerStreamEventtoTurnEvent. - Codex thought phases (
thinking/plan/reasoning/thought) stream asThoughtDelta. - Strict providers suppress streamed assistant chunks when needed so malformed first-pass protocol JSON is not persisted.
- Worker persistence behavior: streamed
ThoughtDeltaandProgressupdates drive transient progress badges and are not appended to session transcript output.
- Claude and Gemini use strict protocol parsing with up to three repair retries when invalid.
- Codex uses permissive parse fallback (schema already supplied via app-server
outputSchemapath).
Clarification Question Loop🔗
- Worker receives final parsed response containing
questionmessages. - Worker persists question list and sets session status
Question. - Reducer switches active view to
AppMode::Questionwhen that session is focused. - User answers each question.
- Runtime builds one follow-up prompt:
Clarifications:
1. Q: <question 1>
A: <response 1>
2. Q: <question 2>
A: <response 2>- Runtime submits this as a normal reply turn; flow returns to standard worker path.
Background Task Catalog🔗
Detached/background execution paths and their trigger conditions:
| Task | Trigger | Spawn site | Emits / Writes | What it does |
|---|---|---|---|---|
| Terminal event reader thread | Runtime startup | runtime/event::spawn_event_reader | Terminal Event channel | Polls crossterm and forwards events; pauses while external editor is open. |
| Git status poller loop | App startup (if project has git branch), and project switch | TaskService::spawn_git_status_task | AppEvent::GitStatusUpdated | Periodic fetch + ahead/behind snapshot (about every 30s). |
| Version check one-shot | App startup | TaskService::spawn_version_check_task | AppEvent::VersionAvailabilityUpdated | Checks npm latest version tag and reports update availability. |
| Per-session worker loop | First command enqueue for a session | SessionWorkerService::spawn_session_worker | DB session_operation updates, app/session updates | Serializes all turn commands per session and manages channel lifecycle. |
| Per-turn turn-event consumer | Every queued turn execution | run_channel_turn | Output append, progress updates, pid slot updates | Consumes TurnEvent stream and applies immediate side effects. |
| CLI stdout/stderr readers | Every CLI-backed turn | CliAgentChannel::run_turn | TurnEvent stream + raw buffers | Reads subprocess streams and emits incremental deltas/progress. |
| App-server stream bridge | Every app-server-backed turn | AppServerAgentChannel::run_turn | TurnEvent stream | Bridges AppServerStreamEvent to unified turn events. |
| Session title generation | First Start turn, before main turn execution | spawn_start_turn_title_generation | DB title + AppEvent::RefreshSessions | Runs one-shot title prompt in background and persists generated title if valid. |
| At-mention file indexing | Prompt input activates @ mention mode | runtime/mode/prompt::activate_at_mention | AppEvent::AtMentionEntriesLoaded | Lists session files (spawn_blocking) and updates mention picker entries. |
| Background session-size refresh | Enter on session in list mode | App::refresh_session_size_in_background | DB size + AppEvent::RefreshSessions | Computes diff-size bucket without blocking key handling path. |
| Deferred session cleanup | Delete with deferred cleanup path | delete_selected_session_deferred_cleanup | Filesystem/git side effects | Removes worktree folder and branch asynchronously after DB deletion. |
| Focused review assist | View mode focused-review toggle when diff is reviewable | TaskService::spawn_focused_review_assist_task | FocusedReviewPrepared / FocusedReviewPreparationFailed | Runs model review prompt and stores final review text or error. |
| Sync-main workflow task | List-mode sync action (s) | TokioSyncMainRunner::start_sync_main | AppEvent::SyncMainCompleted | Pull-rebase/push selected project branch, with assisted conflict flow. |
| Session merge task | Merge confirmation accepted | SessionMergeService::merge_session | Output append, status updates, session metadata updates | Runs rebase + squash merge + worktree cleanup in background. |
| Session rebase task | Rebase action in view mode | SessionMergeService::rebase_session | Output append, status updates | Runs assisted rebase and returns session to Review/Question. |
Sync, Merge, and Rebase Flows🔗
Project and session git workflows use shared boundaries (GitClient, FsClient, assist helpers) but have distinct orchestration paths:
sync main: selected project branch pull/rebase/push, optional assisted conflict resolution, popup result summary.- session merge: queue-aware workflow, assisted rebase first, squash merge into base branch, worktree cleanup, status
Doneon success. - session rebase: assisted rebase of session branch onto base branch, returns to
Reviewafter completion/failure reporting.
Persistence and Recovery Boundaries🔗
Persistence invariants that shape runtime flow:
- DB opens with SQLite WAL and
foreign_keys = ON, then embedded migrations run at startup. - Session snapshots in memory are authoritative for rendering; DB is authoritative for restart recovery.
- Shared session handles (
output,status,child_pid) provide low-latency updates between DB reloads. - Event-driven refresh is primary (
RefreshSessions); metadata polling is fallback safety only. - External integrations (
GitClient,AppServerClient,AgentChannel,EventSource,FsClient,TmuxClient) isolate side effects and enable deterministic tests.